﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using Aeon.Configuration;
using Aeon.Presentation;
using Microsoft.Win32;

namespace Aeon.Emulator.Launcher
{
    /// <summary>
    /// Dialog box for configuration of an Aeon session.
    /// </summary>
    public partial class ConfigEditor : Window
    {
        /// <summary>
        /// The Configuration dependency property definition.
        /// </summary>
        private static readonly DependencyProperty ConfigurationProperty = DependencyProperty.Register("Configuration", typeof(VirtualMachineConfiguration), typeof(ConfigEditor));

        /// <summary>
        /// Initializes a new instance of the ConfigEditor class.
        /// </summary>
        public ConfigEditor()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Gets or sets the configuration to edit.
        /// </summary>
        public VirtualMachineConfiguration Configuration
        {
            get { return (VirtualMachineConfiguration)GetValue(ConfigurationProperty); }
            set { SetValue(ConfigurationProperty, value); }
        }

        /// <summary>
        /// Invoked when a dependency property value has changed.
        /// </summary>
        /// <param name="e">Information about the event.</param>
        protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            base.OnPropertyChanged(e);

            if(e.Property == ConfigurationProperty)
            {
                if(this.Configuration != null)
                    InitializeTree();
            }
        }

        /// <summary>
        /// Initializes the hardware tree.
        /// </summary>
        private void InitializeTree()
        {
            foreach(var device in this.Configuration.Devices)
            {
                var categoryItem = GetHardwareCategory(device.GetType());
                var deviceItem = new TreeViewItem();
                var checkBox = new CheckBox() { Content = device.DeviceName };
                categoryItem.Items.Add(deviceItem);
                deviceItem.Header = checkBox;

                var enabledBinding = new Binding()
                {
                    Source = device,
                    Path = new PropertyPath("IsEnabled")
                };

                checkBox.SetBinding(CheckBox.IsCheckedProperty, enabledBinding);
                AddDeviceProperties(deviceItem, device);
            }
        }

        /// <summary>
        /// Returns the hardware category item with the specified configuration type.
        /// </summary>
        /// <param name="configType">The device configuration type.</param>
        /// <returns>Hardware category TreeViewItem.</returns>
        private TreeViewItem GetHardwareCategory(Type configType)
        {
            string categoryName = null;

            var attributes = configType.GetCustomAttributes(typeof(CategoryAttribute), true);
            if(attributes != null && attributes.Length > 0)
                categoryName = ((CategoryAttribute)attributes[0]).Category;

            if(string.IsNullOrEmpty(categoryName))
                categoryName = "Other";

            var item = (from i in this.hardwareTree.Items.Cast<TreeViewItem>()
                        where i.Header.ToString() == categoryName
                        select i).FirstOrDefault();

            if(item == null)
            {
                item = new TreeViewItem() { Header = categoryName, IsExpanded = true };
                this.hardwareTree.Items.Add(item);
            }

            return item;
        }
        /// <summary>
        /// Adds all of the bindable properties to the hardware tree on a device configuration.
        /// </summary>
        /// <param name="deviceItem">Parent TreeViewItem where properties will be added.</param>
        /// <param name="config">Device configuration instance.</param>
        private void AddDeviceProperties(TreeViewItem deviceItem, IDeviceConfiguration config)
        {
            var properties = config.GetType().GetProperties(BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Instance);
            foreach(var property in properties.Where(p => GetCustomAttribute<BrowsableAttribute, bool>(p, b => b.Browsable)))
            {
                var displayName = GetCustomAttribute<DisplayNameAttribute, string>(property, d => d.DisplayName) ?? property.Name;
                if(property.PropertyType == typeof(int))
                {
                    var minValue = GetCustomAttribute<RangeConstraintAttribute, int>(property, c => c.MinimumValue);
                    var maxValue = GetCustomAttribute<RangeConstraintAttribute, int>(property, c => c.MaximumValue);
                    if(minValue < maxValue)
                    {
                        var propertyItem = new TreeViewItem();
                        deviceItem.Items.Add(propertyItem);
                        var stackPanel = new StackPanel() { Orientation = Orientation.Horizontal };
                        propertyItem.Header = stackPanel;
                        var labelItem = new Label() { Content = displayName + ":" };
                        stackPanel.Children.Add(labelItem);
                        var upDown = new NumericUpDown() { MinimumValue = minValue, MaximumValue = maxValue, Margin = new Thickness(0, 2, 0, 2) };
                        stackPanel.Children.Add(upDown);

                        var valueBinding = new Binding() { Source = config, Path = new PropertyPath(property.Name) };
                        upDown.SetBinding(NumericUpDown.ValueProperty, valueBinding);
                    }
                }
                else
                {
                    var discreteDomain = GetCustomAttribute<DiscreteDomainConstraintAttribute, DiscreteDomainConstraintAttribute>(property, a => a);
                    if(discreteDomain != null)
                    {
                        var propertyItem = new TreeViewItem();
                        deviceItem.Items.Add(propertyItem);
                        var stackPanel = new StackPanel() { Orientation = Orientation.Horizontal };
                        propertyItem.Header = stackPanel;
                        var labelItem = new Label() { Content = displayName + ":" };
                        stackPanel.Children.Add(labelItem);
                        var comboBox = new ComboBox() { Width = 100,  Margin = new Thickness(0, 2, 0, 2), ItemsSource = discreteDomain.GetDomains(property, config) };
                        stackPanel.Children.Add(comboBox);

                        var valueBinding = new Binding() { Source = config, Path = new PropertyPath(property.Name) };
                        comboBox.SetBinding(ComboBox.SelectedValueProperty, valueBinding);
                    }
                }
            }
        }

        /// <summary>
        /// Returns a value read from a custom attribute.
        /// </summary>
        /// <typeparam name="TAttribute">Type of custom attribute.</typeparam>
        /// <typeparam name="TValue">Type of value to return.</typeparam>
        /// <param name="member">Member containing the custom attribute.</param>
        /// <param name="getValue">Method which will return the attribute value.</param>
        /// <returns>Value read from the attribute if the attribute is present; otherwise the default value.</returns>
        private static TValue GetCustomAttribute<TAttribute, TValue>(MemberInfo member, Func<TAttribute, TValue> getValue)
            where TAttribute : Attribute
        {
            var attributes = member.GetCustomAttributes(typeof(TAttribute), false);
            if(attributes != null && attributes.Length > 0)
                return getValue((TAttribute)attributes[0]);
            else
                return default(TValue);
        }

        /// <summary>
        /// Invoked when the OK button is clicked.
        /// </summary>
        /// <param name="sender">Source of the event.</param>
        /// <param name="e">Information about the event.</param>
        private void OKButton_Click(object sender, RoutedEventArgs e)
        {
            var config = this.Configuration;
            if(config != null && this.launchCheckBox.IsChecked == false)
                config.Launch = null;

            this.DialogResult = true;
        }
        /// <summary>
        /// Invoked when the Save button is clicked.
        /// </summary>
        /// <param name="sender">Source of the event.</param>
        /// <param name="e">Information about the event.</param>
        private void SaveButton_Click(object sender, RoutedEventArgs e)
        {
            var dialog = new SaveFileDialog()
            {
                AddExtension = true,
                OverwritePrompt = true,
                DefaultExt = "AeonConfig",
                Filter = "Aeon Configuration files (*.AeonConfig)|*.AeonConfig|All files (*.*)|*.*",
                Title = "Save Configuration..."
            };

            if(dialog.ShowDialog(this) == true)
                this.Configuration.Save(dialog.FileName, LauncherEnvironment.ConfigurationMapper);
        }

        /// <summary>
        /// Invoked when the Load button is clicked.
        /// </summary>
        /// <param name="sender">Source of the event.</param>
        /// <param name="e">Information about the event.</param>
        private void LoadButton_Click(object sender, RoutedEventArgs e)
        {
            var dialog = new OpenFileDialog()
            {
                AddExtension = true,
                CheckFileExists = true,
                DefaultExt = "AeonConfig",
                Filter = "Aeon Configuration files (*.AeonConfig)|*.AeonConfig|All files (*.*)|*.*",
                Title = "Load Configuration..."
            };

            if(dialog.ShowDialog(this) == true)
                this.Configuration = VirtualMachineConfiguration.Load(dialog.FileName, LauncherEnvironment.ConfigurationMapper);
        }
    }
}
